#nullable disable
using System.Data;
using Microsoft.EntityFrameworkCore.Storage;
using Z.EntityFramework.Plus;

namespace Crux.Core.EFCore;
/// <summary>
/// UnitOfWorkOfT
/// </summary>
public class UnitOfWork<TContext>(
        IDbContextFactory<TContext> dbContextFactory,
        IUniqueIdentifierGenerator generator)
    : IUnitOfWork<TContext>
    where TContext : DbContext
{
    #region private fields
    private readonly Lazy<TContext> dbContext = new Lazy<TContext>(() =>
    {
        var context = dbContextFactory.CreateDbContext();
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        context.ChangeTracker.AutoDetectChangesEnabled = true;
        context.ChangeTracker.LazyLoadingEnabled = true;
        context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

        if (context is CruxDbContext cruxDbContext)
        {
            cruxDbContext.Initialize();
        }

        var changedEntriesCopy = context.ChangeTracker?.Entries()?.Where(
                e => e.State == EntityState.Added ||
                    e.State == EntityState.Modified ||
                    e.State == EntityState.Deleted ||
                    e.State == EntityState.Unchanged);
        if (changedEntriesCopy is not null)
        {
            foreach (var entry in changedEntriesCopy)
            {
                entry.State = EntityState.Detached;
            }
        }

        return context;
    });

    private readonly IUniqueIdentifierGenerator generator = generator ?? throw new ArgumentNullException(nameof(generator));
    #endregion  private fields

    #region implements IUnitOfWork
    /// <inheritdoc/>
    public TEntity Add<TEntity>(TEntity entity) where TEntity : class
    {
        Requires.NotNull(entity, nameof(entity));

        if (Context is not CruxDbContext)
        {
            if (entity is IGenericEntity<Guid> guidEntity)
            {
                guidEntity.Id = generator.Generate<Guid>();
            }
            else if (entity is IGenericEntity<long> longEntity)
            {
                longEntity.Id = generator.Generate<long>();
            }
            else if (entity is IGenericEntity<string> stringEntity)
            {
                stringEntity.Id = generator.Generate<string>();
            }
        }

        var entry = Context.Add(entity);

        return entry.Entity;
    }
    /// <inheritdoc/>
    public async Task<TEntity> AddAsync<TEntity>(
        TEntity entity,
        CancellationToken cancellationToken = default) where TEntity : class
    {
        Requires.NotNull(entity, nameof(entity));

        if (Context is not CruxDbContext)
        {
            if (entity is IGenericEntity<Guid> guidEntity)
            {
                guidEntity.Id = generator.Generate<Guid>();
            }
            else if (entity is IGenericEntity<long> longEntity)
            {
                longEntity.Id = generator.Generate<long>();
            }
            else if (entity is IGenericEntity<string> stringEntity)
            {
                stringEntity.Id = generator.Generate<string>();
            }
        }

        var entry = await Context.AddAsync(entity, cancellationToken).ConfigureAwait(false);

        return entry.Entity;
    }

    /// <inheritdoc/>
    public IEnumerable<TEntity> Add<TEntity>(
        IEnumerable<TEntity> entities) where TEntity : class
    {
        Requires.NotNull(entities, nameof(entities));
        Requires.NotNullOrEmpty(entities, nameof(entities));

        foreach (var entity in entities)
        {
            if (Context is not CruxDbContext)
            {
                if (entity is IGenericEntity<Guid> guidEntity)
                {
                    guidEntity.Id = generator.Generate<Guid>();
                }
                else if (entity is IGenericEntity<long> longEntity)
                {
                    longEntity.Id = generator.Generate<long>();
                }
                else if (entity is IGenericEntity<string> stringEntity)
                {
                    stringEntity.Id = generator.Generate<string>();
                }
            }

            Context.Add(entity);
        }

        return entities;
    }

    /// <inheritdoc/>
    public async Task<IEnumerable<TEntity>> AddAsync<TEntity>(
        IEnumerable<TEntity> entities,
        CancellationToken cancellationToken = default) where TEntity : class
    {
        Requires.NotNull(entities, nameof(entities));
        Requires.NotNullOrEmpty(entities, nameof(entities));

        var enumerables = new List<TEntity>();

        foreach (var entity in entities)
        {
            if (Context is not CruxDbContext)
            {
                if (entity is IGenericEntity<Guid> guidEntity)
                {
                    guidEntity.Id = generator.Generate<Guid>();
                }
                else if (entity is IGenericEntity<long> longEntity)
                {
                    longEntity.Id = generator.Generate<long>();
                }
                else if (entity is IGenericEntity<string> stringEntity)
                {
                    stringEntity.Id = generator.Generate<string>();
                }
            }

            var entry = await Context.AddAsync(entity, cancellationToken).ConfigureAwait(false);

            enumerables.Add(entry.Entity);
        }

        return enumerables;
    }
    /// <inheritdoc/>
    public T Update<T>(T entity) where T : class
    {
        Requires.NotNull(entity, nameof(entity));

        EntityEntry<T> trackedEntity = dbContext.Value.ChangeTracker
            .Entries<T>().FirstOrDefault(x => x.Entity == entity);

        if (trackedEntity is null)
        {
            trackedEntity = dbContext.Value.Entry(entity);

            Set<T>().Attach(entity);
            trackedEntity.State = EntityState.Modified;
        }
        else
        {
            var propertyInfos = typeof(T).GetProperties();

            foreach (var propertyInfo in propertyInfos)
            {
                if (propertyInfo.Name.Equals("Id") ||
                    propertyInfo.SetMethod is null)
                    continue;

                var oldValue = propertyInfo.GetValue(trackedEntity.Entity);
                var currentValue = propertyInfo.GetValue(entity);

                if (oldValue is null && currentValue is null)
                    continue;

                propertyInfo.SetValue(trackedEntity.Entity, currentValue);

                if (trackedEntity.Properties.Any(p => p.Metadata.Name == propertyInfo.Name))
                {
                    trackedEntity.Property(propertyInfo.Name).IsModified = true;
                }
            }
        }

        return entity;
    }

    /// <inheritdoc/>
    public IEnumerable<T> UpdateRange<T>(IEnumerable<T> entities) where T : class
    {
        Requires.NotNull(entities, nameof(entities));
        Requires.NotNullOrEmpty(entities, nameof(entities));

        foreach (var entity in entities)
        {
            this.Update(entity);
        }

        return entities;
    }

    /// <inheritdoc/>
    public int Update<T>(
        Expression<Func<T, bool>> where,
        Expression<Func<T, T>> expression) where T : class
    {
        Requires.NotNull(where, nameof(where));
        Requires.NotNull(expression, nameof(expression));

        return Set<T>().Where(where).Update(expression);
    }

    /// <inheritdoc/>
    public async Task<T> UpdateAsync<T>(
        T entity,
        CancellationToken cancellationToken = default) where T : class
    {
        Requires.NotNull(entity, nameof(entity));

        await Task.Delay(0, cancellationToken).ConfigureAwait(false);

        return this.Update(entity);
    }
    /// <inheritdoc/>
    public async Task<IEnumerable<T>> UpdateRangeAsync<T>(
        IEnumerable<T> entities,
        CancellationToken cancellationToken = default) where T : class
    {
        Requires.NotNull(entities, nameof(entities));
        Requires.NotNullOrEmpty(entities, nameof(entities));

        await Task.Delay(0, cancellationToken).ConfigureAwait(false);

        return this.UpdateRange(entities);
    }
    /// <inheritdoc/>
    public async Task<int> UpdateAsync<T>(
        Expression<Func<T, bool>> where,
        Expression<Func<T, T>> expression,
        CancellationToken cancellationToken = default) where T : class
    {
        Requires.NotNull(where, nameof(where));
        Requires.NotNull(expression, nameof(expression));

        return await Set<T>().Where(where).UpdateAsync(expression).ConfigureAwait(false);
    }
    /// <inheritdoc/>
    public DbSet<T> Set<T>() where T : class
        => Context.Set<T>();
    /// <inheritdoc/>
    public T Delete<T>(T entity) where T : class
    {
        Requires.NotNull(entity, nameof(entity));

        Set<T>().Remove(entity);

        return entity;
    }
    /// <inheritdoc/>
    public IEnumerable<T> DeleteRange<T>(IEnumerable<T> entities) where T : class
    {
        Requires.NotNull(entities, nameof(entities));
        Requires.NotNullOrEmpty(entities, nameof(entities));

        foreach (var entity in entities)
        {
            Delete(entity);
        }

        return entities;
    }
    /// <inheritdoc/>
    public int Delete<T>(Expression<Func<T, bool>> where) where T : class
    {
        return Set<T>().Where(where).ExecuteDelete();
    }
    /// <inheritdoc/>
    public async Task<T> DeleteAsync<T>(T entity, CancellationToken cancellationToken = default) where T : class
    {
        Requires.NotNull(entity, nameof(entity));

        await Task.Delay(0, cancellationToken).ConfigureAwait(false);

        return Delete(entity);
    }
    /// <inheritdoc/>
    public async Task<IEnumerable<T>> DeleteRangeAsync<T>(IEnumerable<T> entities, CancellationToken cancellationToken = default) where T : class
    {
        Requires.NotNull(entities, nameof(entities));
        Requires.NotNullOrEmpty(entities, nameof(entities));

        await Task.Delay(0, cancellationToken).ConfigureAwait(false);

        return DeleteRange(entities);
    }
    /// <inheritdoc/>
    public async Task<int> DeleteAsync<T>(Expression<Func<T, bool>> where, CancellationToken cancellationToken = default) where T : class
    {
        Requires.NotNull(where, nameof(where));

        return await Set<T>().Where(where).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
    }
    /// <inheritdoc/>
    public int SoftDelete<T>(Expression<Func<T, bool>> where) where T : class, ISoftDelete
    {
        Requires.NotNull(where, nameof(where));

        return Set<T>().Where(where).ExecuteUpdate(c => c.SetProperty(p => p.Deleted, _ => true));
    }
    /// <inheritdoc/>
    public async Task<int> SoftDeleteAsync<T>(Expression<Func<T, bool>> where, CancellationToken cancellationToken) where T : class, ISoftDelete
    {
        Requires.NotNull(where, nameof(where));

        return await Set<T>().Where(where).ExecuteUpdateAsync(c => c.SetProperty(p => p.Deleted, _ => true), cancellationToken).ConfigureAwait(false);
    }

    /// <inheritdoc/>
    public int PhysicsDelete<T>(Expression<Func<T, bool>> where) where T : class, ISoftDelete
    {
        Requires.NotNull(where, nameof(where));

        return Set<T>().Where(where).ExecuteDelete();
    }

    /// <inheritdoc/>
    public async Task<int> PhysicsDeleteAsync<T>(Expression<Func<T, bool>> where, CancellationToken cancellationToken = default) where T : class, ISoftDelete
    {
        Requires.NotNull(where, nameof(where));

        return await Set<T>().Where(where).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
    }
    /// <inheritdoc/>
    public TContext Context => dbContext.Value;

    /// <inheritdoc/>
    public IDbContextTransaction BeginTranscation()
        => Context.Database.BeginTransaction();
    /// <inheritdoc/>
    public Task<IDbContextTransaction> BeginTranscationAsync(CancellationToken cancellationToken = default)
        => Context.Database.BeginTransactionAsync(cancellationToken: cancellationToken);
    /// <inheritdoc/>
    public Task<int> ExecuteSqlInterpolatedAsync(FormattableString sql, CancellationToken cancellationToken = default)
        => Context.Database.ExecuteSqlInterpolatedAsync(sql, cancellationToken);
    /// <inheritdoc/>
    public Task<int> ExecuteSqlAsync(FormattableString sql, CancellationToken cancellationToken = default)
        => Context.Database.ExecuteSqlAsync(sql, cancellationToken);
    /// <inheritdoc/>
    public int ExecuteSqlRaw(string sql, IEnumerable<object> parameters)
        => Context.Database.ExecuteSqlRaw(sql, parameters);
    /// <inheritdoc/>
    public Task<int> ExecuteSqlRawAsync(string sql, IEnumerable<object> parameters, CancellationToken cancellationToken = default)
        => Context.Database.ExecuteSqlRawAsync(sql, parameters, cancellationToken);
    /// <inheritdoc/>
    public Task<int> ExecuteSqlRawAsync(string sql, params object[] parameters)
        => Context.Database.ExecuteSqlRawAsync(sql, parameters);
    /// <inheritdoc/>
    public Task<int> ExecuteSqlRawAsync(string sql, CancellationToken cancellationToken = default)
        => Context.Database.ExecuteSqlRawAsync(sql, cancellationToken);
    /// <inheritdoc/>
    public int SaveChanges()
        => Context.SaveChanges();
    /// <inheritdoc/>
    public int SaveChanges(bool acceptAllChangesOnSuccess)
        => Context.SaveChanges(acceptAllChangesOnSuccess);
    /// <inheritdoc/>
    public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
        => Context.SaveChangesAsync(cancellationToken);
    /// <inheritdoc/>
    public Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
        => Context.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    /// <inheritdoc/>
    public void ClearChangeTracker()
        => Context.ChangeTracker.Clear();
    /// <inheritdoc/>
    public IUnitOfWork<TContext> SetTimeout(int? timeout)
    {
        Context.Database.SetCommandTimeout(timeout);
        return this;
    }

    #endregion implements IUnitOfWork

    #region implements IDisposableObservable
    /// <inheritdoc/>
    public bool IsDisposed { get; private set; }
    /// <inheritdoc/>
    public void Dispose()
    {
        if (IsDisposed) return;

        IsDisposed = true;
        if (dbContext.IsValueCreated)
        {
            using var _ = Context;
        }

        GC.SuppressFinalize(this);
    }

    /// <inheritdoc/>
    public async ValueTask DisposeAsync()
    {
        if (IsDisposed) return;

        IsDisposed = true;
        if (dbContext.IsValueCreated)
        {
            await using var _ = Context;
        }

        GC.SuppressFinalize(this);
    }
    #endregion implements IDisposableObservable
}